#!/usr/bin/env python

# __  __            _   ____        _ _ ____        _   
#|  \/  | ___  __ _| |_| __ )  __ _| | | __ )  ___ | |_ 
#| |\/| |/ _ \/ _` | __|  _ \ / _` | | |  _ \ / _ \| __|
#| |  | |  __/ (_| | |_| |_) | (_| | | | |_) | (_) | |_ 
#|_|  |_|\___|\__,_|\__|____/ \__,_|_|_|____/ \___/ \__|

# Framework: Circuits
# File: meatball.py                                                            

# Primary contributor: Justin Giorgi (rebel_kid)

# Loved, not licensed

"""This is the Main (startup) file for MeatBallBot. It contains the Main
class which initiates the Network class for each IRC network connection.
The Main class also connects to SQL and polls the Network class clients.
The Network class holds a dictionary of its own instances. Each instance
holds a dictionary of its loaded plugins and an identified dictionary.
Its primary purpose is to provide high level functions and relay IRC
events."""

# Imports dependancies
from psycopg2 import connect as sqlconnect
from sys import argv
from traceback import format_exc
from toolkit import find
from socket import gethostname
from circuits.lib.irc import IRC
from circuits.core import listener, Event, Component, Manager
from circuits.lib.sockets import TCPClient

# Creates event class
class Numeric(Event): pass
class Nick(Event): pass
class Quit(Event): pass
class Message(Event): pass
class Notice(Event): pass
class Ping(Event): pass
class Join(Event): pass
class Part(Event): pass
class Ctcp(Event): pass
class Mode(Event): pass
class Topic(Event): pass
class Invite(Event): pass
class Kick(Event): pass
class TellReply(Event): pass
class IRCquit(Event): pass
class Error(Event): pass

class Main(Component):
	"The main MeatBallBot class which connects to sql, irc, and polls the clients."
	def __init__(self):
		"""Calls the super constructors to initialize the Component and imports
		the error handling Component . Then calls functions to connect to sql
		and irc Then runs a function to poll the clients in a loop, and unregister
		them if they disconnect. All functions are run in a try... except loop.
		Upon except the traceback is passed to error log and an error event is
		triggered."""
		super(Network, self).__init__()
		self.channel = "sudo"
		error = __import__("core/error.py").Plugin(self.channel)
		try:
			self.sql()
			self.connect()
			self.run()
		except:
			self.push(Error(format_exc()), "5013", self.channel)	
			
	def sql(self):
		"""Gets config file data and connects to the sql server. Then sets a long session timeout
		to prevent disconnects due to inactivity and prepares sql statements for this Component.
		Finally returns the sql connection instance and an sql cursor for use in this Component."""	
		db = find("meatballbot", "db")
		debugdb = find("meatballbot", "debugdb")
		user = find("meatballbot", "user")
		passwd = find("meatballbot", "passwd")
		host = find("meatballbot", "host")
		if argv[0] == "debug":
			db = debugdb
		self.sqlconn = sqlconnect("dbname = '%s' user = '%s' host = '%s' password = '%s'" % (db, user, host, passwd))
		self.cursor = self.sqlconn.cursor()
		self.cursor.execute("SET SESSION wait_timeout=99999;")
		self.cursor.execute("PREPARE networks () AS SELECT id, netaddy, port, ssl, nick, password, nickserv FROM networks WHERE active = 1;")
		self.cursor.execute("PREPARE modules () AS SELECT module FROM modules WHERE active = 1;")
		self.cursor.execute("PREPARE network (int) as SELECT id, netaddy, port, ssl, nick, password, nickserv FROM networks WHERE id = $1 AND active = 1;")
		

	def connect(self):
		"Queries networks from the database and creates Network instances for them."
		self.cursor.execute("EXECUTE networks();")
		data = cursor.fetchall()
		for row in data:
			network = Network(data[0], self.sqlconn, data[2], data[3], data[1], "mbb", "MeatBallBot", data[4], data[5], data[6])
			self.manager += network

	def run(self):
		"""Polls the client connection of each Network instance and reconnects to the
		network if disconnected."""
		while True:
			for connection in Network.instances.values():
				if connection.client.connected:
					connection.flush()
					connection.client.poll()
				else:
					connection.newConnect(connection.netid)
					connection.unregister()
					del Network.instances[connection.channel]

class Network(Component):
	
	# Holds network connection instances
	instances = {}

	def __init__(self, netid, sql, port, ssl, host, ident, name, nick, password, nickserv):
		"""1) Initializes component. 2) Sets netid, sql, and irc variables. 3) Connects
		to the network and sets network variables. 4) Stores modinstances and identified
		dictionaries. 5) Imports all plugins in the database."""
		# Important: Call the super constructors to initialize the Component.
		super(Network, self).__init__()
		
		# Sets component variables
		self.netid = netid
		self.channel = str(netid)
		self.sql = sql
		self.cursor = self.sql.cursor()
		Network.instances[self.netid] = self
		
		# Sets irc variables
		self.irc = IRC()
		self.client = TCPClient()
		self += self.irc
		self += self.client
		
		# Connects to the network and assigns network variables
		self.client.open(host, port, ssl)
		self.ident = ident
		self.name = name
		self.nick = nick
		self.password = password
		self.nickserv = nickserv
		
		# Stores instances of modules for the network name:instance
		self.modinstances = {}
		
		# Stores identified for the network (ident, host):nickid
		self.identified = {}
		
		# Imports plugins and core
		self.cursor.execute("EXECUTE modules();")
		data = self.cursor.fetchall()
		for row in data:
			mod = __import__(row[0]).Plugin(self.channel, self.sql.cursor())
			self.manager += mod
			self.modinstances[row[0]] = mod
		
	@listener("connect")
	def onCONNECT(self, host, port):
		"""Sets the ident, name, host and nick for the network then identifies to nickserv
		(if necessary)"""
		self.irc.ircUSER(self.ident, gethostname(), host, self.name)
		self.irc.ircNICK(self.nick)
		if self.nickserv:
			self.irc.ircPRIVMSG("nickserv", "identify %s" % (self.password))
				
	@listener("5001")
	def loadPlugin(self, module):
		"""Imports module and initiates the Plugin class within it.Then adds
		the name variable of the Plugin class to the modinstances dictionary
		with the instance as the key. Then repeats and performs on all network
		instances."""
		for connection in Network.instances.values():
			mod = __import__(module).Plugin(connection.channel, connection.sql.cursor())
			module = mod.name
			connection.manager += mod
			connection.modinstances[module] = mod
	
	@listener("5002")
	def loadPlugin(self, module):
		"""If the module is in the modinstances dictionary its unregister
		function is called and it is deleted from the dictionary. If the
		module is not loaded it is ignored. This is performed on all network
		instances."""
		for connection in Network.instances.values():
			if module in connection.modinstances:
				connection.modinstances[module].unregister()
				del connection.modinstances[module]
				
	@listener("5003")
	def loadPluginOnce(self, module):
		"""Imports module and initiates the Plugin class within it.Then adds
		the name variable of the Plugin class to the modinstances dictionary
		with the instance as the key. This is only performed on the current
		network"""
		mod = __import__(module).Plugin(self.channel, self.sql.cursor())
		module = mod.name
		self.manager += mod
		self.modinstances[module] = mod
		
	@listener("5004")
	def unloadPluginOnce(self, module):
		"""If the module is in the modinstances dictionary its unregister
		function is called and it is deleted from the dictionary. If the
		module is not loaded it is ignored. This is only performed on the
		current network"""
		if module in self.modinstances:
			self.modinstances[module].unregister()
			del self.modinstances[module]
			
	@listener("5005")
	def identifyAdd(self, identhost, nickid):
		"""Adds (ident, host) to the identified dictionary with the nickid
		as the value."""
		self.identified[identhost] = nickid
		
	@listener("5006")
	def identifyDel(self, identhost):
		"""If identhost is in the identified dictionary it is removed. Otherwise
		it is ignored."""
		if identhost in self.identified:
			del self.identified[identhost]
		
	@listener("5007")
	def allQuit(self, message):
		"Quits all IRC connections with the optional message."
		for connection in Network.instances.values():
			connection.push(IRCquit(message), "4007", connection.channel)
			connection.unregister()
			del Network.instances[connection.channel]
			
	@listener("5008")
	def quit(self, message):
		"Quits the current irc connection with the optional message."
		self.push(IRCquit(message), "4007", self.channel)
		self.unregister()
		del Network.instances[self.channel]
		
	@listener("5009")
	def newConnect(self, netid):
		"""Data about the netid is collected from the database and a new
		Network instance is created with it. The instance is registered. If
		no data is found it is ignored."""
		self.cursor.execute("PREPARE network(%d);" % (netid))
		data = self.cursor.fetchone()
		if data is not None:
			network = Network(data[0], self.sqlconn, data[2], data[3], data[1], "mbb", "MeatBallBot", data[4], data[5], data[6])
			manager += network
		
	@listener("5010")
	def allRestart(self, message):
		"""Disconnects from all irc instances and unregisters all network instances.
		Then calls the connect function and reconnects to all networks."""
		for connection in Network.instances.values():
			connection.push(IRCquit(message), "007", connection.channel)
			connection.unregister()
			del Network.instances[connection.channel]
		Main.connect()
		
	@listener("5011")
	def restart(self, message):
		"""Quits IRC then issues a newConnect event with self.netid and unregisters
		the current Network instance."""
		self.push(IRCquit(message), "issue-quit", connection.channel)
		self.newConnect(self.netid)
		self.unregister()
		del Network.instances[self.channel]
			
	@listener("5012")
	def override(self, command):
		"Executes the command as raw python code with no assurances."
		exec(command)
		
	# The rest of the Network class methods just relay events		
	@listener("numeric")
	def onNUMERIC(self, source, target, numeric, arg, message):
		self.push(Numeric(self.identified, source, target, numeric, arg, message), "1001", self.channel)
		
	@listener("nick")
	def onNICK(self, source, newNick, ctime):
		self.push(Nick(self.identified, source, newNick, ctime), "1002", self.channel)
		
	@listener("quit")
	def onQUIT(self, source, message):
		self.push(Quit(self.identified, source, message), "1003", self.channel)
		
	@listener("message")
	def onMESSAGE(self, source, target, message):
		self.push(Message(self.identified, source, target, message), "1004", self.channel)
		
	@listener("notice")
	def onNOTICE(self, source, target, message):
		self.push(Notice(self.identified, source, target, message),  "1005", self.channel)
		
	@listener("ping")
	def onPING(self, server):
		self.push(Ping(self.identified, server), "1006", self.channel)
		
	@listener("join")
	def onJOIN(self, source, channel):
		self.push(Joinself.identified, source, channel), "1007", self.channel)
	
	@listener("part")
	def onPART(self, source, channel, message):
		self.push(Part(self.identified, source, channel, message), "1008", self.channel)
		
	@listener("ctcp")
	def onCTCP(self, source, target, type, message):
		self.push(Ctcp(self.identified, source, target, type, message), "1009", self.channel)
	
	@listener("mode")
	def onMODE(self, ### ASK PROLOGIC ###):
		self.push(Mode(self.identified, ### ASK PROLOGIC ###), "1010", self.channel)
	
	@listener("topic")
	def onTOPIC(self, source, channel, ctime, topic):
		self.push(Topic(self.identified, source, channel, ctime, topic), "1011", self.channel)
		
	@listener("invite")
	def onINVITE(self, ### ASK PROLOGIC ###):
		self.push(Invite(self.identified, ### ASK PROLOGIC ###), "1012", self.channel)
	
	@listener("kick")
	def onKICK(self, ### ASK PROLOGIC ###):
		self.push(Mode(self.identified, ### ASK PROLOGIC ###), "1013", self.channel)

if __name__ == "__main__":
	manager = Manager()
	main = Main()
	manager += main
